#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Xml;
using System.Collections;
using System.Collections.Concurrent;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Net;

using log4net.Util;
using log4net.Repository;
using System.Collections.Generic;

namespace log4net.Config;

/// <summary>
/// Configures a <see cref="ILoggerRepository"/> using an XML tree.
/// </summary>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
public static class XmlConfigurator
{
  /// <summary>
  /// Automatically configures the <see cref="ILoggerRepository"/> using settings
  /// stored in the application's configuration file.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Each application has a configuration file. This has the
  /// same name as the application with '.config' appended.
  /// This file is XML and calling this function prompts the
  /// configurator to look in that file for a section called
  /// <c>log4net</c> that contains the configuration data.
  /// </para>
  /// <para>
  /// To use this method to configure log4net you must specify 
  /// the <see cref="Log4NetConfigurationSectionHandler"/> section
  /// handler for the <c>log4net</c> configuration section. See the
  /// <see cref="Log4NetConfigurationSectionHandler"/> for an example.
  /// </para>
  /// </remarks>
  /// <param name="repository">The repository to configure.</param>
  public static ICollection Configure(ILoggerRepository repository)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  private static void InternalConfigure(ILoggerRepository repository)
  {
    LogLog.Debug(_declaringType, $"configuring repository [{repository.Name}] using .config file section");

    try
    {
      LogLog.Debug(_declaringType, $"Application config file is [{SystemInfo.ConfigurationFileLocation}]");
    }
    catch (Exception e) when (!e.IsFatal())
    {
      // ignore error
      LogLog.Debug(_declaringType, "Application config file location unknown");
    }

    try
    {
      if (System.Configuration.ConfigurationManager.GetSection("log4net") is not XmlElement configElement)
      {
        // Failed to load the xml config using configuration settings handler
        LogLog.Error(_declaringType, "Failed to find configuration section 'log4net' in the application's .config file. Check your .config file for the <log4net> and <configSections> elements. The configuration section should look like: <section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler,log4net\" />");
      }
      else
      {
        // Configure using the xml loaded from the config file
        InternalConfigureFromXml(repository, configElement);
      }
    }
    catch (System.Configuration.ConfigurationException confEx)
    {
      if (confEx.BareMessage.IndexOf("Unrecognized element", StringComparison.Ordinal) >= 0)
      {
        // Looks like the XML file is not valid
        LogLog.Error(_declaringType, "Failed to parse config file. Check your .config file is well formed XML.", confEx);
      }
      else
      {
        // This exception is typically due to the assembly name not being correctly specified in the section type.
        string configSectionStr = "<section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler," + Assembly.GetExecutingAssembly().FullName + "\" />";
        LogLog.Error(_declaringType, "Failed to parse config file. Is the <configSections> specified as: " + configSectionStr, confEx);
      }
    }
  }

  /// <summary>
  /// Automatically configures the log4net system based on the 
  /// application's configuration settings.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Each application has a configuration file. This has the
  /// same name as the application with '.config' appended.
  /// This file is XML and calling this function prompts the
  /// configurator to look in that file for a section called
  /// <c>log4net</c> that contains the configuration data.
  /// </para>
  /// <para>
  /// To use this method to configure log4net you must specify 
  /// the <see cref="Log4NetConfigurationSectionHandler"/> section
  /// handler for the <c>log4net</c> configuration section. See the
  /// <see cref="Log4NetConfigurationSectionHandler"/> for an example.
  /// </para>
  /// </remarks>
  /// <seealso cref="Log4NetConfigurationSectionHandler"/>
  public static ICollection Configure() 
    => Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()));

  /// <summary>
  /// Configures log4net using a <c>log4net</c> element
  /// </summary>
  /// <remarks>
  /// <para>
  /// Loads the log4net configuration from the XML element
  /// supplied as <paramref name="element"/>.
  /// </para>
  /// </remarks>
  /// <param name="element">The element to parse.</param>
  public static ICollection Configure(XmlElement element)
  {
    List<LogLog> configurationMessages = [];

    ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigureFromXml(repository, element);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  /// <summary>
  /// Configures log4net using the specified configuration file.
  /// </summary>
  /// <param name="configFile">The XML file to load the configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration file must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the log4net configuration data.
  /// </para>
  /// <para>
  /// The log4net configuration file can possibly be specified in the application's
  /// configuration file (either <c>MyAppName.exe.config</c> for a
  /// normal application on <c>Web.config</c> for an ASP.NET application).
  /// </para>
  /// <para>
  /// The first element matching <c>&lt;configuration&gt;</c> will be read as the 
  /// configuration. If this file is also a .NET .config file then you must specify 
  /// a configuration section for the <c>log4net</c> element otherwise .NET will 
  /// complain. Set the type for the section handler to <see cref="System.Configuration.IgnoreSectionHandler"/>, for example:
  /// <code lang="XML" escaped="true">
  /// <configSections>
  ///    <section name="log4net" type="System.Configuration.IgnoreSectionHandler" />
  ///  </configSections>
  /// </code>
  /// </para>
  /// <example>
  /// The following example configures log4net using a configuration file, of which the 
  /// location is stored in the application's configuration file :
  /// </example>
  /// <code lang="C#">
  /// using log4net.Config;
  /// using System.IO;
  /// using System.Configuration;
  /// 
  /// ...
  /// 
  /// XmlConfigurator.Configure(new FileInfo(ConfigurationSettings.AppSettings["log4net-config-file"]));
  /// </code>
  /// <para>
  /// In the <c>.config</c> file, the path to the log4net can be specified like this :
  /// </para>
  /// <code lang="XML" escaped="true">
  /// <configuration>
  ///    <appSettings>
  ///      <add key="log4net-config-file" value="log.config"/>
  ///    </appSettings>
  ///  </configuration>
  /// </code>
  /// </remarks>
  public static ICollection Configure(FileInfo configFile)
  {
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(LogManager.GetRepository(Assembly.GetCallingAssembly()), configFile);
    }

    return configurationMessages;
  }

  /// <summary>
  /// Configures log4net using the specified configuration URI.
  /// </summary>
  /// <param name="configUri">A URI to load the XML configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration data must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the log4net configuration data.
  /// </para>
  /// <para>
  /// The <see cref="System.Net.WebRequest"/> must support the URI scheme specified.
  /// </para>
  /// </remarks>
  public static ICollection Configure(Uri configUri)
  {
    List<LogLog> configurationMessages = [];

    ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository, configUri);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  /// <summary>
  /// Configures log4net using the specified configuration data stream.
  /// </summary>
  /// <param name="configStream">A stream to load the XML configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration data must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the log4net configuration data.
  /// </para>
  /// <para>
  /// Note that this method will NOT close the stream parameter.
  /// </para>
  /// </remarks>
  public static ICollection Configure(Stream configStream)
  {
    List<LogLog> configurationMessages = [];

    ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository, configStream);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  /// <summary>
  /// Configures the <see cref="ILoggerRepository"/> using the specified XML 
  /// element.
  /// </summary>
  /// <remarks>
  /// Loads the log4net configuration from the XML element
  /// supplied as <paramref name="element"/>.
  /// </remarks>
  /// <param name="repository">The repository to configure.</param>
  /// <param name="element">The element to parse.</param>
  public static ICollection Configure(ILoggerRepository repository, XmlElement element)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      LogLog.Debug(_declaringType, "configuring repository [" + repository.Name + "] using XML element");

      InternalConfigureFromXml(repository, element);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  /// <summary>
  /// Configures the <see cref="ILoggerRepository"/> using the specified configuration 
  /// file.
  /// </summary>
  /// <param name="repository">The repository to configure.</param>
  /// <param name="configFile">The XML file to load the configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration file must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the configuration data.
  /// </para>
  /// <para>
  /// The log4net configuration file can possibly be specified in the application's
  /// configuration file (either <c>MyAppName.exe.config</c> for a
  /// normal application on <c>Web.config</c> for an ASP.NET application).
  /// </para>
  /// <para>
  /// The first element matching <c>&lt;configuration&gt;</c> will be read as the 
  /// configuration. If this file is also a .NET .config file then you must specify 
  /// a configuration section for the <c>log4net</c> element otherwise .NET will 
  /// complain. Set the type for the section handler to <see cref="System.Configuration.IgnoreSectionHandler"/>, for example:
  /// <code lang="XML" escaped="true">
  /// <configSections>
  ///    <section name="log4net" type="System.Configuration.IgnoreSectionHandler" />
  ///  </configSections>
  /// </code>
  /// </para>
  /// <example>
  /// The following example configures log4net using a configuration file, of which the 
  /// location is stored in the application's configuration file :
  /// </example>
  /// <code lang="C#">
  /// using log4net.Config;
  /// using System.IO;
  /// using System.Configuration;
  /// 
  /// ...
  /// 
  /// XmlConfigurator.Configure(new FileInfo(ConfigurationSettings.AppSettings["log4net-config-file"]));
  /// </code>
  /// <para>
  /// In the <c>.config</c> file, the path to the log4net can be specified like this :
  /// </para>
  /// <code lang="XML" escaped="true">
  /// <configuration>
  ///    <appSettings>
  ///      <add key="log4net-config-file" value="log.config"/>
  ///    </appSettings>
  ///  </configuration>
  /// </code>
  /// </remarks>
  public static ICollection Configure(ILoggerRepository repository, FileInfo configFile)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository, configFile);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  private static void InternalConfigure(ILoggerRepository repository, FileInfo? configFile)
  {
    LogLog.Debug(_declaringType, $"configuring repository [{repository.Name}] using file [{configFile}]");

    if (configFile is null)
    {
      LogLog.Error(_declaringType, "Configure called with null 'configFile' parameter");
    }
    else
    {
      // Have to use File.Exists() rather than configFile.Exists()
      // because configFile.Exists() caches the value, not what we want.
      if (File.Exists(configFile.FullName))
      {
        // Open the file for reading
        FileStream? fs = null;

        // Try hard to open the file
        for (int retry = 5; --retry >= 0;)
        {
          try
          {
            fs = configFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
            break;
          }
          catch (IOException ex)
          {
            if (retry == 0)
            {
              LogLog.Error(_declaringType, $"Failed to open XML config file [{configFile.Name}]", ex);

              // The stream cannot be valid
              fs = null;
            }
            Thread.Sleep(250);
          }
        }

        if (fs is not null)
        {
          try
          {
            // Load the configuration from the stream
            InternalConfigure(repository, fs);
          }
          finally
          {
            // Force the file closed whatever happens
            fs.Dispose();
          }
        }
      }
      else
      {
        LogLog.Debug(_declaringType, "config file [" + configFile.FullName + "] not found. Configuration unchanged.");
      }
    }
  }

  /// <summary>
  /// Configures the <see cref="ILoggerRepository"/> using the specified configuration 
  /// URI.
  /// </summary>
  /// <param name="repository">The repository to configure.</param>
  /// <param name="configUri">A URI to load the XML configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration data must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the configuration data.
  /// </para>
  /// <para>
  /// The <see cref="System.Net.WebRequest"/> must support the URI scheme specified.
  /// </para>
  /// </remarks>
  public static ICollection Configure(ILoggerRepository repository, Uri configUri)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository, configUri);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  private static void InternalConfigure(ILoggerRepository repository, Uri? configUri)
  {
    LogLog.Debug(_declaringType, $"configuring repository [{repository.Name}] using URI [{configUri}]");

    if (configUri is null)
    {
      LogLog.Error(_declaringType, "Configure called with null 'configUri' parameter");
    }
    else
    {
      if (configUri.IsFile)
      {
        // If URI is local file then call Configure with FileInfo
        InternalConfigure(repository, new FileInfo(configUri.LocalPath));
      }
      else
      {
        // NETCF dose not support WebClient
        WebRequest? configRequest = null;

        try
        {
          configRequest = WebRequest.Create(configUri);
        }
        catch (Exception e) when (!e.IsFatal())
        {
          LogLog.Error(_declaringType, $"Failed to create WebRequest for URI [{configUri}]", e);
        }

        if (configRequest is not null)
        {
          // authentication may be required, set client to use default credentials
          try
          {
            configRequest.Credentials = CredentialCache.DefaultCredentials;
          }
          catch (Exception e) when (!e.IsFatal())
          {
            // ignore security exception
          }
          try
          {
            using WebResponse? response = configRequest.GetResponse();
            if (response is not null)
            {
              using Stream configStream = response.GetResponseStream();
              InternalConfigure(repository, configStream);
            }
          }
          catch (Exception e) when (!e.IsFatal())
          {
            LogLog.Error(_declaringType, $"Failed to request config from URI [{configUri}]", e);
          }
        }
      }
    }
  }

  /// <summary>
  /// Configures the <see cref="ILoggerRepository"/> using the specified configuration 
  /// file.
  /// </summary>
  /// <param name="repository">The repository to configure.</param>
  /// <param name="configStream">The stream to load the XML configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration data must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the configuration data.
  /// </para>
  /// <para>
  /// Note that this method will NOT close the stream parameter.
  /// </para>
  /// </remarks>
  public static ICollection Configure(ILoggerRepository repository, Stream configStream)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigure(repository, configStream);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  private static void InternalConfigure(ILoggerRepository repository, Stream? configStream)
  {
    LogLog.Debug(_declaringType, $"configuring repository [{repository.Name}] using stream");

    if (configStream is null)
    {
      LogLog.Error(_declaringType, "Configure called with null 'configStream' parameter");
    }
    else
    {
      // Load the config file into a document
      XmlDocument? doc = new() { XmlResolver = null };
      try
      {
        // Allow the DTD to specify entity includes
        XmlReaderSettings settings = new()
        {
          // .NET 4.0 warning CS0618: 'System.Xml.XmlReaderSettings.ProhibitDtd'
          // is obsolete: 'Use XmlReaderSettings.DtdProcessing property instead.'
          DtdProcessing = DtdProcessing.Ignore
        };

        // Create a reader over the input stream
        using XmlReader xmlReader = XmlReader.Create(configStream, settings);

        // load the data into the document
        doc.Load(xmlReader);
      }
      catch (Exception e) when (!e.IsFatal())
      {
        LogLog.Error(_declaringType, "Error while loading XML configuration", e);

        // The document is invalid
        doc = null;
      }

      if (doc is not null)
      {
        LogLog.Debug(_declaringType, "loading XML configuration");

        // Configure using the 'log4net' element
        XmlNodeList configNodeList = doc.GetElementsByTagName("log4net");
        if (configNodeList.Count == 0)
        {
          LogLog.Debug(_declaringType, "XML configuration does not contain a <log4net> element. Configuration Aborted.");
        }
        else if (configNodeList.Count > 1)
        {
          LogLog.Error(_declaringType, $"XML configuration contains [{configNodeList.Count}] <log4net> elements. Only one is allowed. Configuration Aborted.");
        }
        else
        {
          InternalConfigureFromXml(repository, configNodeList[0] as XmlElement);
        }
      }
    }
  }

  /// <summary>
  /// Configures log4net using the file specified, monitors the file for changes 
  /// and reloads the configuration if a change is detected.
  /// </summary>
  /// <param name="configFile">The XML file to load the configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration file must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the configuration data.
  /// </para>
  /// <para>
  /// The configuration file will be monitored using a <see cref="FileSystemWatcher"/>
  /// and depends on the behavior of that class.
  /// </para>
  /// <para>
  /// For more information on how to configure log4net using
  /// a separate configuration file, see <see cref="Configure(FileInfo)"/>.
  /// </para>
  /// </remarks>
  /// <seealso cref="Configure(FileInfo)"/>
  public static ICollection ConfigureAndWatch(FileInfo configFile)
  {
    List<LogLog> configurationMessages = [];

    ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigureAndWatch(repository, configFile);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  /// <summary>
  /// Configures the <see cref="ILoggerRepository"/> using the file specified, 
  /// monitors the file for changes and reloads the configuration if a change 
  /// is detected.
  /// </summary>
  /// <param name="repository">The repository to configure.</param>
  /// <param name="configFile">The XML file to load the configuration from.</param>
  /// <remarks>
  /// <para>
  /// The configuration file must be valid XML. It must contain
  /// at least one element called <c>log4net</c> that holds
  /// the configuration data.
  /// </para>
  /// <para>
  /// The configuration file will be monitored using a <see cref="FileSystemWatcher"/>
  /// and depends on the behavior of that class.
  /// </para>
  /// <para>
  /// For more information on how to configure log4net using
  /// a separate configuration file, see <see cref="Configure(FileInfo)"/>.
  /// </para>
  /// </remarks>
  /// <seealso cref="Configure(FileInfo)"/>
  public static ICollection ConfigureAndWatch(ILoggerRepository repository, FileInfo configFile)
  {
    repository.EnsureNotNull();
    List<LogLog> configurationMessages = [];

    using (new LogLog.LogReceivedAdapter(configurationMessages))
    {
      InternalConfigureAndWatch(repository, configFile);
    }

    repository.ConfigurationMessages = configurationMessages;

    return configurationMessages;
  }

  private static void InternalConfigureAndWatch(ILoggerRepository repository, FileInfo? configFile)
  {
    LogLog.Debug(_declaringType, $"configuring repository [{repository.Name}] using file [{configFile}] watching for file updates");

    if (configFile is null)
    {
      LogLog.Error(_declaringType, "ConfigureAndWatch called with null 'configFile' parameter");
    }
    else
    {
      // Configure log4net now
      InternalConfigure(repository, configFile);

      try
      {
        // Support multiple repositories each having their own watcher.
        // Create and start a watch handler that will reload the
        // configuration whenever the config file is modified.
        _repositoryName2ConfigAndWatchHandler.AddOrUpdate(
          configFile.FullName,
          _ => new ConfigureAndWatchHandler(repository, configFile),
          (_, handler) =>
          {
            // Replace the old handler.
            handler.Dispose();
            return new ConfigureAndWatchHandler(repository, configFile);
          });
      }
      catch (Exception e) when (!e.IsFatal())
      {
        LogLog.Error(_declaringType, $"Failed to initialize configuration file watcher for file [{configFile.FullName}]", e);
      }
    }
  }

  /// <summary>
  /// Class used to watch config files.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Uses the <see cref="FileSystemWatcher"/> to monitor
  /// changes to a specified file. Because multiple change notifications
  /// may be raised when the file is modified, a timer is used to
  /// compress the notifications into a single event. The timer
  /// waits for <see cref="TimeoutMillis"/> time before delivering
  /// the event notification. If any further <see cref="FileSystemWatcher"/>
  /// change notifications arrive while the timer is waiting it
  /// is reset and waits again for <see cref="TimeoutMillis"/> to
  /// elapse.
  /// </para>
  /// </remarks>
  private sealed class ConfigureAndWatchHandler : IDisposable
  {
    /// <summary>
    /// Holds the FileInfo used to configure the XmlConfigurator
    /// </summary>
    private readonly FileInfo _configFile;

    /// <summary>
    /// Holds the repository being configured.
    /// </summary>
    private readonly ILoggerRepository _repository;

    /// <summary>
    /// The timer used to compress the notification events.
    /// </summary>
    private readonly Timer _timer;

    /// <summary>
    /// The default amount of time to wait after receiving notification
    /// before reloading the config file.
    /// </summary>
    private const int TimeoutMillis = 500;

    /// <summary>
    /// Watches file for changes. This object should be disposed when no longer
    /// needed to free system handles on the watched resources.
    /// </summary>
    private readonly FileSystemWatcher _watcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="ConfigureAndWatchHandler" /> class to
    /// watch a specified config file used to configure a repository.
    /// </summary>
    /// <param name="repository">The repository to configure.</param>
    /// <param name="configFile">The configuration file to watch.</param>
    /// <remarks>
    /// <para>
    /// Initializes a new instance of the <see cref="ConfigureAndWatchHandler" /> class.
    /// </para>
    /// </remarks>
    [System.Security.SecuritySafeCritical]
    public ConfigureAndWatchHandler(ILoggerRepository repository, FileInfo configFile)
    {
      this._repository = repository;
      this._configFile = configFile;

      // Create a new FileSystemWatcher and set its properties.
      _watcher = new FileSystemWatcher()
      {
        Path = this._configFile.DirectoryName.EnsureNotNull(),
        Filter = this._configFile.Name,
        NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.FileName,
      };

      // Add event handlers. OnChanged will do for all event handlers that fire a FileSystemEventArgs
      _watcher.Changed += ConfigureAndWatchHandler_OnChanged;
      _watcher.Created += ConfigureAndWatchHandler_OnChanged;
      _watcher.Deleted += ConfigureAndWatchHandler_OnChanged;
      _watcher.Renamed += ConfigureAndWatchHandler_OnRenamed;

      // Begin watching.
      _watcher.EnableRaisingEvents = true;

      // Create the timer that will be used to deliver events. Set as disabled
      _timer = new Timer(OnWatchedFileChange, state: null, Timeout.Infinite, Timeout.Infinite);
    }

    /// <summary>
    /// Event handler used by <see cref="ConfigureAndWatchHandler"/>.
    /// </summary>
    /// <param name="source">The <see cref="FileSystemWatcher"/> firing the event.</param>
    /// <param name="e">The argument indicates the file that caused the event to be fired.</param>
    /// <remarks>
    /// <para>
    /// This handler reloads the configuration from the file when the event is fired.
    /// </para>
    /// </remarks>
    private void ConfigureAndWatchHandler_OnChanged(object source, FileSystemEventArgs e)
    {
      LogLog.Debug(_declaringType, $"ConfigureAndWatchHandler: {e.ChangeType} [{_configFile.FullName}]");

      // Deliver the event in TimeoutMillis time
      // timer will fire only once
      _timer.Change(TimeoutMillis, Timeout.Infinite);
    }

    /// <summary>
    /// Event handler used by <see cref="ConfigureAndWatchHandler"/>.
    /// </summary>
    /// <param name="source">The <see cref="FileSystemWatcher"/> firing the event.</param>
    /// <param name="e">The argument indicates the file that caused the event to be fired.</param>
    /// <remarks>
    /// <para>
    /// This handler reloads the configuration from the file when the event is fired.
    /// </para>
    /// </remarks>
    private void ConfigureAndWatchHandler_OnRenamed(object source, RenamedEventArgs e)
    {
      LogLog.Debug(_declaringType, $"ConfigureAndWatchHandler: {e.ChangeType} [{_configFile.FullName}]");

      // Deliver the event in TimeoutMillis time
      // timer will fire only once
      _timer.Change(TimeoutMillis, Timeout.Infinite);
    }

    /// <summary>
    /// Called by the timer when the configuration has been updated.
    /// </summary>
    /// <param name="state">null</param>
    private void OnWatchedFileChange(object? state) => InternalConfigure(_repository, _configFile);

    /// <summary>
    /// Release the handles held by the watcher and timer.
    /// </summary>
    [System.Security.SecuritySafeCritical]
    public void Dispose()
    {
      _watcher.EnableRaisingEvents = false;
      _watcher.Dispose();
      _timer.Dispose();
    }
  }

  /// <summary>
  /// Configures the specified repository using a <c>log4net</c> element.
  /// </summary>
  /// <param name="repository">The hierarchy to configure.</param>
  /// <param name="element">The element to parse.</param>
  /// <remarks>
  /// <para>
  /// Loads the log4net configuration from the XML element
  /// supplied as <paramref name="element"/>.
  /// </para>
  /// <para>
  /// This method is ultimately called by one of the Configure methods 
  /// to load the configuration from an <see cref="XmlElement"/>.
  /// </para>
  /// </remarks>
  private static void InternalConfigureFromXml(ILoggerRepository? repository, XmlElement? element)
  {
    if (element is null)
    {
      LogLog.Error(_declaringType, "ConfigureFromXml called with null 'element' parameter");
    }
    else if (repository is null)
    {
      LogLog.Error(_declaringType, "ConfigureFromXml called with null 'repository' parameter");
    }
    else
    {
      LogLog.Debug(_declaringType, "Configuring Repository [" + repository.Name + "]");

      if (repository is not IXmlRepositoryConfigurator configurableRepository)
      {
        LogLog.Warn(_declaringType, "Repository [" + repository + "] does not support the XmlConfigurator");
      }
      else
      {
        // Copy the xml data into the root of a new document
        // this isolates the xml config data from the rest of
        // the document
        XmlDocument newDoc = new() { XmlResolver = null };
        XmlElement newElement = newDoc.AppendChild(newDoc.ImportNode(element, true)).EnsureIs<XmlElement>();

        // Pass the configurator the config element
        configurableRepository.Configure(newElement);
      }
    }
  }

  /// <summary>
  /// Maps repository names to ConfigAndWatchHandler instances to allow a particular
  /// ConfigAndWatchHandler to dispose of its FileSystemWatcher when a repository is 
  /// reconfigured.
  /// </summary>
  private static readonly ConcurrentDictionary<string, ConfigureAndWatchHandler> _repositoryName2ConfigAndWatchHandler = new(StringComparer.Ordinal);

  /// <summary>
  /// The fully qualified type of the XmlConfigurator class.
  /// </summary>
  /// <remarks>
  /// Used by the internal logger to record the Type of the
  /// log message.
  /// </remarks>
  private static readonly Type _declaringType = typeof(XmlConfigurator);
}
